Skip to content

fix(web/agent-chat): default tool_call/tool_result off, add toolbar toggle#6388

Merged
theonlyhennygod merged 4 commits intozeroclaw-labs:masterfrom
singlerider:fix/6348-dashboard-tool-call-not-inline
May 6, 2026
Merged

fix(web/agent-chat): default tool_call/tool_result off, add toolbar toggle#6388
theonlyhennygod merged 4 commits intozeroclaw-labs:masterfrom
singlerider:fix/6348-dashboard-tool-call-not-inline

Conversation

@singlerider
Copy link
Copy Markdown
Collaborator

@singlerider singlerider commented May 5, 2026

Summary

The dashboard's Agent chat was rendering every tool_call and tool_result WebSocket frame as a standalone card in the message stream, including errored tool calls (e.g. memory_recall returning "Provide at least 'query' ..."), alongside the user message and the assistant reply. That noise belongs in an activity surface, not the conversation transcript.

Two changes:

  • Gate the inline render of tool_call and tool_result behind a showToolActivity flag (default false). The flag persists to localStorage.zeroclaw_show_tool_activity for cross-session memory.
  • Add a discoverable Wrench toggle to the chat toolbar next to Compact, with aria-pressed semantics and agent.tool_activity_show / agent.tool_activity_hide i18n strings. Operators can flip the gate without dropping into devtools.

Audit of the full WsMessage union confirms tool_call and tool_result are the only frames that should be gated as plumbing. cron_result is asynchronous output from a job the operator explicitly scheduled, not internal-to-turn agent reasoning, so it stays in the transcript unconditionally.

Closes #6348

Validation Evidence

cd web && npm run build

Builds cleanly (only the pre-existing chunk-size warnings).

Security & Privacy Impact

  • New permissions, capabilities, or filesystem access scope? No.
  • New external network calls? No.
  • Secrets / tokens / credentials handling changed? No.
  • PII or real identities in diff, tests, fixtures, or docs? No.

Compatibility

  • Backward compatible? Yes. Default behaviour stops surfacing the noisy cards; users who want the prior behaviour click the Wrench toggle (or set localStorage.zeroclaw_show_tool_activity = '1').
  • Config / env / CLI surface changed? No.

Rollback

git revert <merge-sha>. Two-file diff (web/src/pages/AgentChat.tsx, web/src/lib/i18n.ts). Operators who specifically want the noisy cards back set the localStorage flag.

…ript

Tool execution is plumbing. The dashboard Agent chat was rendering
every tool_call and tool_result frame as its own card in the message
stream alongside user messages and the assistant reply, including
errored tool calls (e.g. memory_recall returning "Provide at least
'query' ..."). That noise belongs in an activity surface, not the
conversation transcript.

Gate inline rendering of tool_call / tool_result behind a new
showToolActivity flag, default false. Read from localStorage at
'zeroclaw_show_tool_activity'; flip to '1' to opt in.

Not in this commit (deliberately separate so the immediate noise stops
landing in the transcript before the larger UX work):

- Toggle UI for showToolActivity (settings panel or chat-header
  control).
- Routing tool activity into the existing collapsed "Thinking" strip
  so power users have a discoverable home for it.
- Resyncing chatHistoryStorage so already-persisted tool_call cards
  on disk also disappear on reload (today they survive in localStorage
  if the user had the bug-rendered cards persisted before this fix).

Refs zeroclaw-labs#6348.
@singlerider singlerider requested a review from Audacity88 May 5, 2026 04:17
@singlerider singlerider added bug Something isn't working gateway Auto scope: src/gateway/** changed. tool Auto scope: src/tools/** changed. labels May 5, 2026
@singlerider singlerider self-assigned this May 5, 2026
@singlerider singlerider added risk: medium Auto risk: src/** or dependency/config changes. size: XS Auto size: <=80 non-doc changed lines. labels May 5, 2026
@github-actions github-actions Bot removed gateway Auto scope: src/gateway/** changed. tool Auto scope: src/tools/** changed. labels May 5, 2026
Adds a discoverable Wrench button next to the existing Compact toggle
that flips the same `zeroclaw_show_tool_activity` localStorage flag the
gates already read. Operators no longer have to drop into devtools to
opt back in to inline tool_call / tool_result cards.

Audited the full WsMessage union while wiring the toggle: tool_call
and tool_result are the only frames that should be gated as
"plumbing." cron_result is asynchronous output from a job the operator
explicitly scheduled, not internal-to-turn agent reasoning, so it
stays in the transcript unconditionally. Updated the gate comment so
it does not promise a deferred toggle UI that now exists.

Refs zeroclaw-labs#6348
Comments now describe what the gate does, not what PR introduced it.
@singlerider singlerider added this to the v0.7.5 milestone May 5, 2026
@singlerider singlerider changed the title fix(web/agent-chat): default tool_call/tool_result off in chat transcript (#6348) fix(web/agent-chat): default tool_call/tool_result off, add toolbar toggle May 5, 2026
@singlerider singlerider marked this pull request as ready for review May 5, 2026 06:28
Copy link
Copy Markdown
Collaborator

@Audacity88 Audacity88 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I reviewed the live PR state for #6388 at head f487936, the linked issue #6348, the current diff for AgentChat.tsx / i18n.ts, existing reviews/comments, and the failing CI job log. There are no prior reviews or inline threads yet. I did not run a local web build because the source-level issue below blocks the advertised toggle behavior.

🟢 What looks good — default hiding matches the issue

Hiding tool_call and tool_result frames by default is the right direction for #6348. The distinction in the PR body also looks right: routine tool execution is chat plumbing, while cron_result is an explicit scheduled-job output and should remain visible in the transcript.

🔴 Blocking — the Wrench toggle uses stale state in the WebSocket handler

The new toggle changes React state, but the WebSocket message handler that gates tool_call and tool_result is installed once in the connection effect with an empty dependency list. That handler closes over the initial showToolActivity value from page load.

So with the default false setting, clicking the Wrench button changes the visible button state and localStorage, but the existing ws.onMessage callback still sees showToolActivity === false. Future tool_call and tool_result frames keep getting skipped until the component remounts or the socket handler is recreated. That breaks the second half of this PR: the discoverable opt-in toggle does not actually opt the current chat session back into inline tool activity.

Could you make the message handler read the current flag at message time? A small showToolActivityRef kept in sync with state would fit this pattern, or the socket callback can be safely reattached when the flag changes as long as it does not leak or reconnect unnecessarily.

🟡 Check status — the red 32-bit job looks like CI noise, but it still needs a rerun

The only failing required check I saw is Check (32-bit). Its log shows the job was canceled while installing gcc-multilib, before the Rust check reached this PR's code. Since this is a web-only diff, I do not think that failure is evidence against the patch itself, but the branch still needs a green required-gate rerun before merge.

Copy link
Copy Markdown
Collaborator

@WareWolf-MoonWall WareWolf-MoonWall left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — #6388 fix(web/agent-chat): default tool_call/tool_result off, add toolbar toggle

Verdict: comment
Reviewer: WareWolf-MoonWall
Head checked: f487936 (2/12 checks failing)


I read the full diff (AgentChat.tsx, i18n.ts), the PR body, the linked issue #6348, the existing review (Audacity88: CHANGES_REQUESTED), the failing CI log, and the current AgentChat.tsx source in the local codebase to verify the closure structure. I'm not approving over Audacity88's active block.

State of the queue: Audacity88 holds an active CHANGES_REQUESTED on the stale-closure bug. That finding is correct. I'm seconding it and adding a couple of supplementary observations.


On Audacity88's block — I agree and verified it

I read the useEffect(() => {...}, []) block in AgentChat.tsx. The WebSocket connection is established once, and ws.onMessage is assigned inside that effect. The new showToolActivity state is captured at the moment the effect runs — which is the initial render, where showToolActivity is false (the localStorage default). When the user clicks the Wrench toggle, setShowToolActivity updates the React state and re-renders the button correctly, but the existing ws.onMessage callback still holds the closure over the initial false value. New tool_call and tool_result frames keep getting dropped until the component remounts. The toggle is visually responsive but functionally inert for the current session.

The fix Audacity88 described is the right one. Here's the specific pattern, consistent with how pendingContentRef and capturedThinkingRef are already used in this file:

const showToolActivityRef = useRef(showToolActivity);
useEffect(() => {
  showToolActivityRef.current = showToolActivity;
}, [showToolActivity]);

Then inside ws.onMessage, replace if (!showToolActivity) with if (!showToolActivityRef.current). The ref sync effect is lightweight (no socket reconnect, no message loss), and it keeps the WS effect's [] dependency array intact — which is the right choice since reconnecting the socket every time the toggle changes would be disruptive.

Note: the default-off behavior (showToolActivity === false at mount) works correctly as shipped. The stale closure only affects the toggle-on path. This means the primary fix from #6348 — stopping the noise of tool activity cards in normal use — is already functional. The broken part is opt-in visibility, not the default behavior.


🟡 Warning — inline style prop is inconsistent with the rest of the toolbar

The new Wrench button uses:

style={{ padding: '0.3rem 0.75rem', borderRadius: '0.5rem' }}

The existing Compact button alongside it uses only Tailwind class names for layout and sizing. The mixed approach (Tailwind + inline styles) is inconsistent and will make the button harder to retheme or restyle with the rest of the toolbar. The padding and border-radius values here correspond to standard Tailwind classes (px-3 py-1.5 rounded-lg or similar). Prefer using class names for consistency with the surrounding component.


🔵 Suggestion — button label text when default (off) is potentially confusing

When showToolActivity is false (the default), the button displays t('agent.tool_activity_show') — "Show tool activity". That's correct. But a first-time user looking at the toolbar might read "Show tool activity" as a status indicator (tool activity is showing) rather than a call to action (click to show tool activity). The aria-label correctly uses the action framing. The visible label does too, but it might be worth a quick UX check before landing. This is a minor 🔵, not a blocker.


🟡 Warning — CI must be green before merge

Two checks are failing. Audacity88 identified the Check (32-bit) failure as CI infrastructure noise (cancelled during gcc-multilib install, never reached the Rust check). That analysis looks correct for a web-only diff. But both failing checks need a clean rerun result before this can merge, regardless of the root cause. Please trigger a rerun and confirm green.


🟢 Praise — cron_result explicitly kept out of the gate

The PR body documents why cron_result is treated differently from tool_call/tool_result: it's the output of a job the operator scheduled, not internal agent reasoning. That distinction is correct and explicitly justified. Not gating it prevents silent loss of scheduled-job output when the toggle is off. The reasoning is right, and it's recorded where a future maintainer can find it.


🟢 Praise — toggleToolActivity correctly uses functional update

The toggleToolActivity callback is:

const toggleToolActivity = useCallback(() => {
  setShowToolActivity((prev) => {
    const next = !prev;
    try { localStorage.setItem(...); } catch { /* noop */ }
    return next;
  });
}, []);

The empty dependency array is correct here because the functional update form (prev) => !prev doesn't close over showToolActivity — it always acts on the current state value. The localStorage write inside the updater is a side effect in an updater function, which React can call twice in StrictMode, but since writing the same derived value twice is idempotent in localStorage, this is fine in practice.


Template

The PR body fully fills the required sections. The cron_result audit rationale in the body is the kind of explicit scope reasoning that makes future changes safer. The rollback section correctly names both files and the localStorage escape hatch. The compatibility section is accurate.


Summary: Audacity88's stale-closure block is the right call and I've verified it in the source. The default-off behavior works today; it's the toggle-on path that needs the ref fix. Address that, clean up the inline style, trigger a CI rerun, and this is ready for re-review.

…rrent session

Per @Audacity88 / @WareWolf-MoonWall review: the WebSocket onMessage handler
is installed once with `[]` deps, so it closed over `showToolActivity`'s
initial value (false from the localStorage default). Clicking the Wrench
toggle updated the button state but the handler kept dropping `tool_call`
and `tool_result` frames until the component remounted.

Mirrors `showToolActivity` into a `showToolActivityRef` synced via a small
effect, then reads `showToolActivityRef.current` inside the handler. Same
pattern already used for `pendingContentRef` and `capturedThinkingRef` in
this file, so the WS effect's `[]` dependency array stays intact and the
socket does not reconnect on every toggle.
Copy link
Copy Markdown
Collaborator

@theonlyhennygod theonlyhennygod left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed current head f20b9c7. I read the PR body, linked issue #6348, @Audacity88's CHANGES_REQUESTED at f487936, @WareWolf-MoonWall's COMMENTED at f487936, and the full diff (AgentChat.tsx, i18n.ts).

✅ [resolved] Audacity88's blocker — stale-closure bug fixed

@Audacity88's review at f487936 correctly diagnosed that the WebSocket onMessage handler closed over the initial showToolActivity = false and never picked up subsequent toggle changes. The current diff implements exactly the ref-mirror pattern they (and @WareWolf-MoonWall) recommended:

const showToolActivityRef = useRef(showToolActivity);
useEffect(() => { showToolActivityRef.current = showToolActivity; }, [showToolActivity]);

And inside the handler:

case 'tool_call': {
  if (!showToolActivityRef.current) {
    break;
  }
  ...
}
case 'tool_result': {
  if (!showToolActivityRef.current) {
    break;
  }
  ...
}

Same pattern for both tool_call and tool_result. The WS effect's [] dependency stays intact (no socket reconnect-per-toggle), the ref sync effect is lightweight, and the toggle now actually opts the current session back into inline tool activity. The fix matches the suggested shape line-for-line.

✅ [resolved] CI is green at HEAD

@Audacity88's 🟡 about the Check (32-bit) failure being CI infrastructure noise looks correct in retrospect — the current required gate is fully green at f20b9c7.

🟡 [warning] Inline style prop on the new Wrench button

Same observation @WareWolf-MoonWall raised; still applies at HEAD. The new button uses:

style={{ padding: '0.3rem 0.75rem', borderRadius: '0.5rem' }}

while the adjacent Compact / Clear All / Delete buttons use Tailwind utility classes for layout and sizing. The mixed approach makes the Wrench button harder to retheme alongside the rest of the toolbar (e.g., a future rounded-md → rounded-lg sweep would skip this one). The values map cleanly to standard Tailwind: px-3 py-1.5 rounded-lg (or whatever the existing toolbar uses — copy the Compact button's class string and you're done).

Not blocking — this is a single button on a UX surface that's already shipped — but worth tightening before more buttons land alongside it and the inconsistency compounds.

🔵 [suggestion] Button label phrasing

@WareWolf-MoonWall's 🔵 about "Show tool activity" reading as a status indicator vs. a call to action is still worth a quick UX check. The aria-pressed semantics (true when activity is showing, false when hidden) plus the action-framed aria-label already do the right thing for screen readers; the visible label is the part that's slightly ambiguous to a sighted user seeing the toolbar for the first time. Optional — could go either way.

🟢 What's working well

  • cron_result explicitly kept out of the gate, with the rationale documented in the PR body — that distinction (operator-scheduled job output ≠ internal agent plumbing) is correct and worth recording where a future maintainer can find it.
  • toggleToolActivity uses the functional updater form (prev) => !prev with an empty useCallback dep array — that's the right shape, and the localStorage write inside the updater is idempotent so StrictMode double-invocation is harmless.
  • Default-off behavior was already functionally correct (the stale-closure bug only affected the toggle-on path), so the primary fix from #6348 — stopping the noise of tool cards in normal use — was working even at the prior head. The fix here is for the opt-in visibility path.

Verdict

Audacity88's blocker is fully addressed. The two open items are a 🟡 (inline style hygiene) and a 🔵 (label phrasing) — neither blocks merge.

Leaving as --comment rather than --approve because @Audacity88's CHANGES_REQUESTED is still on the books at f487936. @Audacity88, the ref-mirror pattern landed exactly as you suggested; would you take another look at HEAD f20b9c7 to convert or dismiss your review?

@theonlyhennygod
Copy link
Copy Markdown
Collaborator

@Audacity88 — quick re-review ping. The stale-closure blocker you raised at f487936 is addressed at HEAD f20b9c7: the Wrench handler now reads showToolActivityRef.current (the ref-mirror pattern you suggested) instead of the closure variable, so the toggle works for the current session. CI is green. Details in the review I just posted above — converting or dismissing your CHANGES_REQUESTED clears the merge gate. Thanks!

@singlerider singlerider dismissed Audacity88’s stale review May 6, 2026 10:18

Resolved at f20b9c7 — added showToolActivityRef mirrored from state via a small effect; ws.onMessage reads showToolActivityRef.current so the Wrench toggle re-enables tool activity in the running session without needing a remount.

@singlerider singlerider requested a review from Audacity88 May 6, 2026 10:22
Copy link
Copy Markdown
Collaborator

@WareWolf-MoonWall WareWolf-MoonWall left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review — #6388 fix(web/agent-chat): default tool_call/tool_result off, add toolbar toggle

Verdict: approve
Reviewer: WareWolf-MoonWall
Head checked: f20b9c7 (CI green, all checks passing)


I reviewed the full diff at f20b9c7, the prior review thread (Audacity88 at f487936, my comment at f487936, theonlyhennygod's comment at f20b9c7), and the updated AgentChat.tsx source.


✅ RESOLVED — @Audacity88's blocking finding (stale closure)

The showToolActivityRef pattern is correctly implemented:

const showToolActivityRef = useRef(showToolActivity);
useEffect(() => { showToolActivityRef.current = showToolActivity; }, [showToolActivity]);

And inside the WS handler:

case 'tool_call': {
  if (!showToolActivityRef.current) { break; }
  ...
}
case 'tool_result': {
  if (!showToolActivityRef.current) { break; }
  ...
}

This is exactly the pattern I verified against in the source and described in my prior comment. The WS effect's [] dependency array is intact (no socket reconnect per toggle), the ref sync effect is lightweight, and the Wrench toggle now actually opts the current session into inline tool activity. The stale-closure bug is fully fixed.

✅ RESOLVED — CI green

Both failing checks from the prior head are gone. All required gates pass at f20b9c7. The Check (32-bit) infrastructure noise @Audacity88 identified is no longer blocking.


🟢 Default-off behavior was already correct; the fix completes the opt-in path

The primary fix from #6348 — stopping tool_call / tool_result noise in normal use — was functional even at the stale-closure head. f20b9c7 fixes the toggle-on path so operators who want inline tool activity can actually reach it. The scope of the change is exactly right: two frames gated (plumbing), cron_result stays unconditional (operator-scheduled output).

🟢 toggleToolActivity uses the functional update form correctly

setShowToolActivity((prev) => !prev) with an empty useCallback dep array is correct — no closure over state, idempotent localStorage write, StrictMode-safe. This is the right shape.


🟡 Inline style on the Wrench button — still present, not blocking

Both @theonlyhennygod and I flagged this in prior reviews. The Wrench button still uses:

style={{ padding: '0.3rem 0.75rem', borderRadius: '0.5rem' }}

while the surrounding Compact / Clear All / Delete buttons use Tailwind class names. The values map to something like px-3 py-1.5 rounded-lg — copy the Compact button's class string and the inconsistency is gone. I'm not blocking on it, but I'd encourage addressing it before more toolbar buttons are added and the pattern compounds.


@Audacity88 — your CHANGES_REQUESTED at f487936 is shown as dismissed. For clarity: the ref-mirror fix that addresses your original block has been in place since f20b9c7. This approval proceeds on the strength of that fix.

Milestone: v0.7.5 is set — correct, no change needed.

@theonlyhennygod theonlyhennygod merged commit 89d3707 into zeroclaw-labs:master May 6, 2026
12 checks passed
theonlyhennygod added a commit to songchao0421/zeroclaw that referenced this pull request May 6, 2026
Resolves the merge conflict with zeroclaw-labs#6388 (tool_call/tool_result toggle).
Integrates the Wrench button + showToolActivity state into the new
useAgent() consumer pattern from this PR.

Conflict resolution in web/src/pages/AgentChat.tsx:
- Imports: kept ChevronDown (model dropdown) + added Wrench (zeroclaw-labs#6388).
- State: kept useAgent() consumption; dropped showToolActivityRef
  since the WebSocket lifecycle now lives in AgentContext (this PR),
  not in AgentChat — the ref was only needed when onMessage closed
  over the toggle.
- Removed the obsolete local WebSocket useEffect from AgentChat;
  hydration / streaming / tool_call / tool_result handling all run
  in AgentContext per this PR's architecture.
- Render: filter messages with .toolCall out of the rendered list
  when showToolActivity is off. Slight UX change vs. the original
  zeroclaw-labs#6388: tool cards are always present in context state, so toggling
  on retroactively reveals prior tool activity.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working risk: medium Auto risk: src/** or dependency/config changes. size: XS Auto size: <=80 non-doc changed lines.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Dashboard Agent chat surfaces every tool_call inline as a chat message

4 participants